/**
 * Copyright 2009-2010 by The Regents of the University of California
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * you may obtain a copy of the License from
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package edu.ucr.cs.dblab.acestore.base;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.io.FileUtils;

/**
 * Storage Manager.
 * 
 * A storage manager handles all operations related to the file system.
 * Such operations include:
 * <ul>
 * <li> Load the list of indices in the specified database location;
 * <li> Create a new database;
 * <li> Create a new file for a new index;
 * <li> Load a file;
 * <li> Delete a file.
 * <ul>
 * 
 * @created Jan 9, 2011
 */
public class StorageManager {
	
	/**
	 * List of index ids.
	 */
	private Map<Integer, String> indexList;
	/**
	 * The location of the folder containing data files.
	 */
	public final String dbLocation;
	
	private int pageSize;
	
	private int diskRead, diskWrite;
	
	/**
	 * Constructor with specified location.
	 * 
	 * This constructor will load the meta data file from the specified
	 * database location, and get all the index ids.
	 * 
	 * @param dbLocation
	 * @throws IOException
	 */
	public StorageManager(String dbLocation) throws IOException{
		this.dbLocation = dbLocation;
		this.open();
	}
	
	public StorageManager(int pageSize) throws IOException{
		this.pageSize = pageSize;
		// Create a new database storage
		this.dbLocation = ACEConfiguration.DBPATH + UUID.randomUUID().toString() + "/";
		File dbPath = new File(this.dbLocation);
		dbPath.mkdir();
		this.initMeta();
	}
	
	public void initMeta() throws IOException{
		this.indexList = new HashMap<Integer, String>();
		diskRead = diskWrite = 0;
	}
	
	/**
	 * Open the storage manager.
	 * 
	 * @throws IOException
	 */
	public void open() throws IOException{
		// Initialize the accessor to the meta data file.
		RandomAccessFile raf = new RandomAccessFile(dbLocation + "DB_META", "r");
		this.pageSize = raf.readInt();
		int indexCount = raf.readInt();
		this.indexList = new HashMap<Integer, String>();
		if(indexCount > 0){
			for(int i = 0; i < indexCount; i++){
				indexList.put(raf.readInt(), raf.readUTF());
			}
		}
		raf.close();
		diskRead = diskWrite = 0;
	}
	
	/**
	 * Reset the disk IO counters.
	 */
	public void resetIOs(){
		diskRead = diskWrite = 0;
	}
	
	/**
	 * Close the storage manager. 
	 * The meta data about the indices will be written back to the meta data file.
	 * 
	 * @throws IOException
	 */
	public void close() throws IOException{
		
		// Write the meta data back to the meta file
		RandomAccessFile raf = new RandomAccessFile(dbLocation + "DB_META", "rw");
		raf.writeInt(pageSize);
		raf.writeInt(indexList.size());
		for(int k : indexList.keySet()){
			raf.writeInt(k);
			raf.writeUTF(indexList.get(k));
		}
		raf.close();
	}

	/**
	 * Load a page.
	 * 
	 * @param idxID
	 * @param pageID
	 * @return
	 * @throws IOException
	 */
	public ByteBuffer load (int idxID, int pageID) throws IOException{
		String fileName = indexList.get(idxID);
		if(fileName == null){
			throw new FileNotFoundException("Cannot find the data file for index " + idxID);
		}
		RandomAccessFile raf = new RandomAccessFile(dbLocation + fileName, "r");
		raf.seek(pageSize * pageID);
		byte[] buf = new byte[pageSize];
		int len = raf.read(buf);
		if(len != pageSize){
			throw new IOException("Failed to load a page with inconsistance length: (" + len + ":" + pageSize + ")");
		}
		raf.close();
		diskRead ++;
		return ByteBuffer.wrap(buf);
	}
	
	/**
	 * Save a page.
	 * 
	 * @param idxID
	 * @param pageID
	 * @param buf
	 * @throws IOException
	 */
	public void save (int idxID, int pageID, ByteBuffer buf) throws IOException {
		if(buf.capacity() != pageSize){
			throw new IOException("Failed to write a page with inconsistance length: (" + buf.capacity() + ":" + pageSize + ")");
		}
		String fileName = indexList.get(idxID);
		while(fileName == null){
			throw new FileNotFoundException("Cannot find the data file for index " + idxID);
		}
		RandomAccessFile raf = new RandomAccessFile(dbLocation + fileName, "rw");
		raf.seek(pageSize * pageID);
		raf.write(buf.array());
		diskWrite++;
		raf.close();
	}
	
	/**
	 * Allocate a file for a new index. 
	 * 
	 * Note that this operation only adds a new entry to
	 * the meta data. The actual file will be added when
	 * some data is written into this file.
	 * 
	 * @return	The ID of this file.
	 */
	public int allocateFile() {
		int idxID = 0;
		while(indexList.containsKey(idxID))
			idxID ++;
		String fileName = UUID.randomUUID().toString();
		indexList.put(idxID, fileName);
		return idxID;
	}
	
	/**
	 * Deallocate a file for a specified index ID.
	 * 
	 * @param idxID	The ID of the index to be deleted.
	 * @throws IOException
	 */
	public void deallocateFile(int idxID) throws IOException {
		if(!indexList.containsKey(idxID)){
			throw new IOException("No such index (" + idxID + ") to be deallocated!");
		}
		String fileName = indexList.get(idxID);
		File idxFile = new File(dbLocation + fileName);
		idxFile.delete();
	}
	
	public String listMeta(){
		StringBuilder sbder = new StringBuilder();
		for(int k : indexList.keySet()){
			sbder.append(k + "\t" + indexList.get(k) + "\n");
		}
		return sbder.toString();
	}
	
	public String getIOStat(){
		return "Disk Read\t" + this.diskRead + "\nDisk Write\t" + this.diskWrite + "\nDisk Space\t" + this.getStorageSize() +"\n";
	}

	public int getPageSize() {
		return this.pageSize;
	}
	
	/**
	 * 
	 * @return The size of the data space in bytes
	 */
	public long getStorageSize() {
	    return FileUtils.sizeOfDirectory(new File(this.dbLocation));
	}
}
